package com.joinway.framework.extension.net.ftp;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.List;

import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.joinway.framework.extension.net.type.FtpMode;
import com.joinway.framework.extension.net.type.FtpTransferMode;

public class FtpClient {

	private final static Logger log = LoggerFactory.getLogger(FtpClient.class);
	
	protected String host;
	
	protected int port;
	
	protected String username;
	
	protected String password;
	
	protected FtpMode mode;
	
	protected FTPClient client;
	
	protected boolean loggedIn;
	
	protected int dataTimeout = 30;		// in seconds
	
	protected int defaultTimeout = 30;	// in seconds
	
	protected int connectTimeout = 30;	// in seconds		
	
	protected int retryCount = 3;
	
	public FtpClient(String host){
		this(host, 21);
	}

	public FtpClient(String host, FtpClientConfig config){
		this(host, config.getPort(), config.getUsername(), config.getPassword(), config.getMode());
		this.retryCount = config.getRetryCount();
	}
	
	public FtpClient(String host, int port){
		this(host, port, "anonymous");
	}
	
	public FtpClient(String host, int port, String username){
		this(host, port, username, "", FtpMode.Passive);
	}
	
	public FtpClient(String host, String username){
		this(host, 21, username);
	}
	
	public FtpClient(String host, String username, String password){
		this(host, 21, username, password);
	}
	
	public FtpClient(String host, int port, String username, String password){
		this(host, port, username, password, FtpMode.Passive);
	}
	
	public FtpClient(String host, String username, String password, FtpMode mode){
		this(host, 21, username, password, mode);
	}
	
	public FtpClient(String host, int port, String username, String password, FtpMode mode){
		this.username = username;
		this.password = password;
		this.host = host;
		this.port = port;
		this.mode = mode;
		this.loggedIn = false;
		
		client = new FTPClient();
		
//		client.setUseEPSVwithIPv4(false);
		
//		client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
//		client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(new StringWriter(){
//			@Override
//			public void write(String text) {
////				log.info(text);
//				super.write(text);
//			}
//
//			@Override
//			public StringWriter append(CharSequence arg0) {
//				log.info(arg0.toString());
//				return super.append(arg0);
//			}
//		}), true));
		client.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out){
			@Override
			public void print(String arg0) {
				log.info(arg0);
				super.print(arg0);
			}
			
		}, true));
		
		initTimeouts();
	}
	
	public boolean login() throws FtpClientException {
		boolean value = false;
		
		try{
			initTimeouts();
			
			if(!client.isConnected()){
				connect();
			}
			
			value = client.login(username, password);
			
			loggedIn = true;
		}catch(Exception e){
			throw new FtpClientException(e);
		}
		
		return value;
	}
	
	public boolean logout() throws FtpClientException {
		return logout(false);
	}
	
	/**
	 * 
	 * @param disconnect
	 * @return
	 * @throws Exception
	 */
	public boolean logout(boolean disconnect) throws FtpClientException {
		boolean value = true;
		
		try{
			loggedIn = false;
			
			if(client.isConnected()){
				value = client.logout();
				
				if(disconnect){
					client.disconnect();
				}
			}
		}catch(Exception e){
			throw new FtpClientException(e);
		}
		
		return value;
	}
	
	public void connect() throws FtpClientException {
		initTimeouts();
		
		processWithRetry(new FtpClientRetryCallback<Void>(){
			@Override
			public Void process() throws Exception {
				client.connect(host, port);
				
				int code = client.getReplyCode();
				if (!FTPReply.isPositiveCompletion(code)){
					throw new IOException("FTP server " + host + " refused connection.");
				}
				
				return null;
			}
		});
		
		initMode();
	}
	
	public void disconnect() throws FtpClientException {
		loggedIn = false;
		try{
			client.disconnect();
		}catch(Exception e){
			throw new FtpClientException(e);
		}
	}
	
	public boolean upload(String localFile, FtpTransferMode mode) throws FtpClientException {
		return upload(localFile, null, mode);
	}
	
	public boolean upload(String localFile, String remotePath, FtpTransferMode mode) throws FtpClientException {
		return upload(localFile, remotePath, null, mode);
	}
	
	public boolean upload(String localFile, String remotePath, String remoteFileName, FtpTransferMode mode) throws FtpClientException {
		boolean value = false;
		
		FileInputStream fis = null;
		
		try{
			initTimeouts();
			prepareFileTransfer(remotePath, mode);
			
			File file = new File(localFile);
			final String remoteTarget = remoteFileName == null || remoteFileName.trim().length() == 0 ? file.getName() : remoteFileName;
			
			final FileInputStream input = new FileInputStream(file);
			fis = input;
			
			value = processWithRetry(new FtpClientRetryCallback<Boolean>(){
				@Override
				public Boolean process() throws Exception {
					return client.storeFile(remoteTarget, input);
				}
			});
		}catch(Exception e){
			throw new FtpClientException(e);
		}finally{
			if(fis != null){
				try{
					fis.close();
				}catch(Exception e){
					log.error("close upload stream failed");
				}
			}
		}
		
		return value;
	}
	
	public boolean download(String remoteFileName, String localFile, FtpTransferMode mode) throws FtpClientException {
		return download(null, remoteFileName, localFile, mode);
	}

	public boolean download(String remotePath, final String remoteFileName, String localFile, FtpTransferMode mode) throws FtpClientException {
		boolean value = false;
		FileOutputStream fos = null;
		
		try{
			initTimeouts();
			prepareFileTransfer(remotePath, mode);
			
			String localTarget = localFile == null || localFile.trim().length() == 0 ? remoteFileName : localFile;
			
			File out = new File(localTarget);
			if(out.isDirectory()){
				out.mkdirs();
			}else{
				out.getParentFile().mkdirs();
			}
			final FileOutputStream output = new FileOutputStream(localTarget);
			fos = output;
			
			value = processWithRetry(new FtpClientRetryCallback<Boolean>(){
				@Override
				public Boolean process() throws Exception {
					return client.retrieveFile(remoteFileName, output);
				}
			});
					
		}catch(Exception e){
			throw new FtpClientException(e);
		}finally{
			if(fos != null){
				try{
					fos.close();
				}catch(Exception e){
					log.error("failed to close download stream");
				}
			}
		}
		
		return value;
	}
	
	public boolean delete(String remotePath, String remoteFileName) throws FtpClientException {
		return delete(remotePath + "/" + remoteFileName);
	}
	
	public boolean delete(final String remoteFilePathname) throws FtpClientException  {
		boolean value = false;
		initTimeouts();
		
		processWithRetry(new FtpClientRetryCallback<Boolean>(){
			@Override
			public Boolean process() throws Exception {
				return client.deleteFile(remoteFilePathname);
			}
		});
		 
		return value;
	}
	
	public List<String> listFiles() throws FtpClientException {
		List<String> files = null;
		
		try{
			files = Arrays.asList(client.listNames());
		}catch(Exception e){
			throw new FtpClientException(e);
		}
		return files;
	}

	public List<String> listFiles(String remotePath) throws FtpClientException {
		List<String> files = null;
		
		try{
			files = Arrays.asList(client.listNames(remotePath));
		}catch(Exception e){
			throw new FtpClientException(e);
		}
		return files;
	}
	
	protected void prepareFileTransfer(String remotePath, FtpTransferMode mode) throws FtpClientException  {
		int fileType = FtpTransferMode.Ascii == mode ? FTP.ASCII_FILE_TYPE : FTP.BINARY_FILE_TYPE;

		try{
			client.setFileType(fileType);
			
			if(remotePath != null && remotePath.trim().length() > 0){
				client.changeWorkingDirectory(remotePath);
			}
		}catch(Exception e){
			throw new FtpClientException(e);
		}
	}

	protected void initTimeouts(){
		client.setDataTimeout(dataTimeout * 1000);
		client.setDefaultTimeout(defaultTimeout * 1000);
		client.setConnectTimeout(connectTimeout * 1000);
	}
	
	protected void initMode(){
		if(mode == FtpMode.Passive){
			client.enterLocalPassiveMode();
		}else{
			client.enterLocalActiveMode();
		}
	}
	
	protected<T> T processWithRetry(FtpClientRetryCallback<T> callback) throws FtpClientException {
		int count = 0;
		
		T t = null;
		
		do{
			try{
				t = callback.process();
				break;
			}catch(Exception e){
				if(++count >= retryCount){
					throw new FtpClientException(e);
				}
			}

		}while(true);
		
		return t;
	}
	
	public int getDataTimeout() {
		return dataTimeout;
	}

	public void setDataTimeout(int dataTimeout) {
		this.dataTimeout = dataTimeout;
	}

	public int getDefaultTimeout() {
		return defaultTimeout;
	}

	public void setDefaultTimeout(int defaultTimeout) {
		this.defaultTimeout = defaultTimeout;
	}

	public int getConnectTimeout() {
		return connectTimeout;
	}

	public void setConnectTimeout(int connectTimeout) {
		this.connectTimeout = connectTimeout;
	}

	public boolean isLoggedIn() {
		return loggedIn;
	}
}
